﻿module TrainingData.PolarHrm.Hrm

open System
open TrainingData.Common.Model
open TrainingData.Common.UnitsOfMeasure

let private kilometresPerMile = 1.609344m

let private metresPerFoot = 0.3048m

let private addElement el s = seq {
    yield! s
    yield el
}

let private getSectionHeaderAndContents ( str : string ) =
    let posFirstBreak = str.IndexOf('\n')
    let header = str.Substring(1, posFirstBreak - 1 - 2)
    let contents = str.Substring(posFirstBreak + 1)
    (header, contents)

let private split ( labels : string list ) ( contents : string ) =
    labels |> List.map (fun l -> contents.IndexOf(l))
           |> addElement (contents.Length - 1)
           |> Seq.pairwise
           |> Seq.map (fun (posStart, posEnd) -> contents.Substring(posStart, posEnd - posStart))
           |> Seq.map getSectionHeaderAndContents
           |> Map.ofSeq

type Units = 
    | Euro
    | US

type Mode = {
    Units : Units
    Speed : bool
    Cadence : bool
    Altitude : bool
    Power : bool
    PowerBalance : bool
}

type Params = {
    Version : int
    Monitor : int
    Mode : Mode
    StartTime : DateTime
    Duration : TimeSpan
    Interval : TimeSpan
}

type Lap = {
    Time : TimeSpan
    CurrentHR : decimal<beat/min>
    MinimumHR : decimal<beat/min>
    MeanHR : decimal<beat/min>
    MaximumHR : decimal<beat/min>
}

type HrmFile = {
    Parameters : Params
    Note : string
    LapTimes : Lap list
    Data : DataType list list
}

let private readParams ( paramsStr : string ) =
    let lines = paramsStr.Split([| "\r\n" |], StringSplitOptions.RemoveEmptyEntries)
    let values = lines |> Array.map (fun l -> let equalsPos = l.IndexOf("=")
                                              let k = l.Substring(0, equalsPos)
                                              let v = l.Substring(equalsPos + 1)
                                              (k, v))
                        |> Map.ofArray

    let parseMode ( version : int ) ( str : string ) =
        let parseTrueFalse = function
            | '0' -> false
            | '1' -> true
            | _   -> failwith "Invalid string"

        match version with
        | 106 -> { Units = match str.[7] with
                           | '0' -> Euro
                           | '1' -> US
                           | _   -> failwith "Invalid string"
                   Speed = str.[0] |> parseTrueFalse
                   Cadence = str.[1] |> parseTrueFalse
                   Altitude = str.[2] |> parseTrueFalse
                   Power = str.[3] |> parseTrueFalse
                   PowerBalance = str.[4] |> parseTrueFalse }
        | 102
        | 105
        | 107 -> failwith "Not handled"
        | _   -> failwith "Invalid version"

    let version = values.["Version"] |> Int32.Parse
    let monitor = values.["Monitor"] |> Int32.Parse
    let mode = parseMode version values.["SMode"]

    let parseTime timeStr =
        let m = Text.RegularExpressions.Regex.Match(timeStr, @"(\d+):(\d\d):(\d\d).(\d)")
        let hours = m.Groups.[1].Value |> Int32.Parse
        let minutes = m.Groups.[2].Value |> Int32.Parse
        let seconds = m.Groups.[3].Value |> Int32.Parse
        let tenths = m.Groups.[4].Value |> Int32.Parse
        TimeSpan(0, hours, minutes, seconds, 100 * tenths)

    let date = let dateStr = values.["Date"]
               let year = dateStr.Substring(0, 4) |> Int32.Parse
               let month = dateStr.Substring(4, 2) |> Int32.Parse
               let day = dateStr.Substring(6, 2) |> Int32.Parse
               let time = values.["StartTime"] |> parseTime
               DateTime(year, month, day, 0, 0, 0, DateTimeKind.Local).Add(time)

    let duration = values.["Length"] |> parseTime
    let interval = let t = values.["Interval"] |> Int32.Parse
                   TimeSpan(0, 0, t)

    { Version = values.["Version"] |> Int32.Parse
      Monitor = values.["Monitor"] |> Int32.Parse
      Mode =  values.["SMode"] |> parseMode version
      StartTime = date
      Duration = duration
      Interval = interval }

let private readLapTimes ( lapStr : string ) =
    let groupIntoLength n sequence =
        let length = sequence |> System.Linq.Enumerable.Count
        let noOfGroups = max (length / n + (if length % n = 0 then 0 else 1)) 1
        seq { for i in 0 .. (noOfGroups - 1) do
              yield System.Linq.Enumerable.Skip(sequence, i * n) |> Seq.truncate n
                                                                 |> List.ofSeq }
              |> List.ofSeq

    let lines = lapStr.Split([| "\r\n" |], StringSplitOptions.RemoveEmptyEntries) |> List.ofArray
                                                                                 |> List.filter (fun s -> s |> String.IsNullOrWhiteSpace |> not)
    let laps = lines |> groupIntoLength 5
                     |> List.map (fun ls -> let h::_ = ls
                                            let m = Text.RegularExpressions.Regex.Match(h, "(\d+:\d+:\d+.\d+)\t(\d+)\t(\d+)\t(\d+)\t(\d+)")
                                            let time = m.Groups.[1].Value |> TimeSpan.Parse
                                            let current = m.Groups.[2].Value |> Decimal.Parse |> (*) 1m<beat/min>
                                            let min = m.Groups.[3].Value |> Decimal.Parse |> (*) 1m<beat/min>
                                            let mean = m.Groups.[4].Value |> Decimal.Parse |> (*) 1m<beat/min>
                                            let max = m.Groups.[5].Value |> Decimal.Parse |> (*) 1m<beat/min>
                                            { Time = time
                                              CurrentHR = current
                                              MinimumHR = min
                                              MeanHR = mean
                                              MaximumHR = max })
    laps    

let private readHrData mode ( hrStr : string ) = 
    let lines = hrStr.Split([| "\r\n" |], StringSplitOptions.RemoveEmptyEntries) |> List.ofArray
                                                                                 |> List.filter (fun s -> s |> String.IsNullOrWhiteSpace |> not)

    let data = lines |> List.map (fun l -> let values = l.Split('\t') |> List.ofArray
                                                                      |> List.map (fun e -> Decimal.Parse(e))

                                           let values, hr = values.Tail, values.Head |> (*) 1m<beat/min> |> HeartRate |> Some
                                           let values, speed = match mode.Speed with
                                                               | false -> values, None
                                                               | true  -> let speedConstant = match mode.Units with
                                                                                              | Euro -> 1m<km/h>
                                                                                              | US   -> 1m<km/h> / kilometresPerMile
                                                                          values.Tail, values.Head |> (*) 0.1m |> (*) speedConstant |> Speed |> Some
                                           let values, cadence = match mode.Cadence with
                                                                 | false -> values, None
                                                                 | true  -> values.Tail, values.Head |> (*) 1m<rev/min> |> BikeCadence |> Some
                                           let values, altitude = match mode.Altitude with
                                                                  | false -> values, None
                                                                  | true  -> let altitudeConstant = match mode.Units with
                                                                                                    | Euro -> 1m
                                                                                                    | US   -> 1m / metresPerFoot
                                                                             values.Tail, values.Head |> (*) 1m<m> |> (*) altitudeConstant |> Some
                                           let values, power = match mode.Power with
                                                               | false -> values, None
                                                               | true  -> match mode.PowerBalance with
                                                                          | false -> let power = values.Head |> (*) 1m<W>
                                                                                     values.Tail, Power(power, None) |> Some
                                                                          | true  -> let power = values.Head |> (*) 1m<W>
                                                                                     let values = values.Tail
                                                                                     let powerBalance = values.Head |> Some
                                                                                     values.Tail, Power(power, powerBalance) |> Some

                                           [ hr; speed; cadence; power ] |> List.choose id)
    data

let load ( contents : string ) =
    let sectionNames = [ "Params"; "Note"; "HRZones"; "IntTimes"; "IntNotes"; "Summary-123"; "Summary-TH"; "HRData" ] |> List.map (fun s -> sprintf "[%s]" s)
    let sections = contents |> split sectionNames

    let parameters = sections.["Params"] |> readParams
    let note = sections.["Note"]

    let lapData = sections.["IntTimes"] |> readLapTimes

    let hrData = sections.["HRData"] |> readHrData parameters.Mode

    { Parameters = parameters
      Note = note
      LapTimes = lapData
      Data = hrData }
